# -*- coding: binary -*-
module Msf

require 'msf/core/exploit/tcp'

###
#
# This module exposes methods that may be useful to exploits that deal with
# servers that speak the File Transfer Protocol (FTP).
#
###
module Exploit::Remote::Ftp

  include Exploit::Remote::Tcp

  #
  # Creates an instance of an FTP exploit module.
  #
  def initialize(info = {})
    super

    # Register the options that all FTP exploits may make use of.
    register_options(
      [
        Opt::RHOST,
        Opt::RPORT(21),
        OptString.new('FTPUSER', [ false, 'The username to authenticate as', 'anonymous']),
        OptString.new('FTPPASS', [ false, 'The password for the specified username', 'mozilla@example.com'])
      ], Msf::Exploit::Remote::Ftp)

    register_advanced_options(
      [
        OptInt.new('FTPTimeout', [ true, 'The number of seconds to wait for a reply from an FTP command', 16]),
        OptBool.new('FTPDEBUG', [ false, 'Whether or not to print verbose debug statements', false ])
      ], Msf::Exploit::Remote::Ftp)

    register_autofilter_ports([ 21, 2121])
    register_autofilter_services(%W{ ftp })

    @ftpbuff = ""

  end

  #
  # This method establishes an FTP connection to host and port specified by
  # the 'rhost' and 'rport' methods. After connecting, the banner
  # message is read in and stored in the 'banner' attribute.
  #
  def connect(global = true, verbose = nil)
    verbose ||= datastore['FTPDEBUG']
    verbose ||= datastore['VERBOSE']

    print_status("Connecting to FTP server #{rhost}:#{rport}...") if verbose

    fd = super(global)

    # Wait for a banner to arrive...
    self.banner = recv_ftp_resp(fd)

    print_status("Connected to target FTP server.") if verbose

    # Return the file descriptor to the caller
    fd
  end

  #
  # This method handles establishing datasocket for data channel
  #
  def data_connect(mode = nil, nsock = self.sock)
    if mode
      res = send_cmd([ 'TYPE' , mode ], true, nsock)
      return nil if not res =~ /^200/
    end

    # force datasocket to renegotiate
    self.datasocket.shutdown if self.datasocket != nil

    res = send_cmd(['PASV'], true, nsock)
    return nil if not res =~ /^227/

    # 227 Entering Passive Mode (127,0,0,1,196,5)
    if res =~ /\((\d+)\,(\d+),(\d+),(\d+),(\d+),(\d+)/
      # convert port to FTP syntax
      datahost = "#{$1}.#{$2}.#{$3}.#{$4}"
      dataport = ($5.to_i * 256) + $6.to_i
      self.datasocket = Rex::Socket::Tcp.create(
        'PeerHost' => datahost,
        'PeerPort' => dataport,
        'Context'  => { 'Msf' => framework, 'MsfExploit' => self }
      )
    end
    self.datasocket
  end

  #
  # This method handles disconnecting our data channel
  #
  def data_disconnect
    self.datasocket.shutdown if self.datasocket
    self.datasocket = nil
  end

  #
  # Connect and login to the remote FTP server using the credentials
  # that have been supplied in the exploit options.
  #
  def connect_login(global = true, verbose = nil)
    verbose ||= datastore['FTPDEBUG']
    verbose ||= datastore['VERBOSE']
    ftpsock = connect(global, verbose)

    if !(user and pass)
      print_error("No username and password were supplied, unable to login")
      return false
    end

    print_status("Authenticating as #{user} with password #{pass}...") if verbose
    res = send_user(user, ftpsock)

    if (res !~ /^(331|2)/)
      print_error("The server rejected our username") if verbose
      return false
    end

    if (pass)
      print_status("Sending password...") if verbose
      res = send_pass(pass, ftpsock)
      if (res !~ /^2/)
        print_error("The server rejected our password") if verbose
        return false
      end
    end

    return true
  end

  #
  # This method logs in as the supplied user by transmitting the FTP
  # 'USER <user>' command.
  #
  def send_user(user, nsock = self.sock)
    raw_send("USER #{user}\r\n", nsock)
    recv_ftp_resp(nsock)
  end

  #
  # This method completes user authentication by sending the supplied
  # password using the FTP 'PASS <pass>' command.
  #
  def send_pass(pass, nsock = self.sock)
    raw_send("PASS #{pass}\r\n", nsock)
    recv_ftp_resp(nsock)
  end

  #
  # This method sends a QUIT command.
  #
  def send_quit(nsock = self.sock)
    raw_send("QUIT\r\n", nsock)
    recv_ftp_resp(nsock)
  end

  #
  # This method sends one command with zero or more parameters
  #
  def send_cmd(args, recv = true, nsock = self.sock)
    cmd = args.join(" ") + "\r\n"
    ret = raw_send(cmd, nsock)
    if (recv)
      return recv_ftp_resp(nsock)
    end
    return ret
  end

  #
  # This method transmits the command in args and receives / uploads DATA via data channel
  # For commands not needing data, it will fall through to the original send_cmd
  #
  # For commands that send data only, the return will be the server response.
  # For commands returning both data and a server response, an array will be returned.
  #
  # NOTE: This function always waits for a response from the server.
  #
  def send_cmd_data(args, data, mode = 'a', nsock = self.sock)
    type = nil
    # implement some aliases for various commands
    if (args[0] =~ /^DIR$/i || args[0] =~ /^LS$/i)
      # TODO || args[0] =~ /^MDIR$/i || args[0] =~ /^MLS$/i
      args[0] = "LIST"
      type = "get"
    elsif (args[0] =~ /^GET$/i)
      args[0] = "RETR"
      type = "get"
    elsif (args[0] =~ /^PUT$/i)
      args[0] = "STOR"
      type = "put"
    end

    # fall back if it's not a supported data command
    if not type
      return send_cmd(args, true, nsock)
    end

    # Set the transfer mode and connect to the remove server
    return nil if not data_connect(mode)

    # Our pending command should have got a connection now.
    res = send_cmd(args, true, nsock)
    # make sure could open port
    return nil unless res =~ /^(150|125) /

    # dispatch to the proper method
    if (type == "get")
      # failed listings just disconnect..
      begin
        data = self.datasocket.get_once(-1, ftp_timeout)
      rescue ::EOFError
        data = nil
      end
    else
      sent = self.datasocket.put(data)
    end

    # close data channel so command channel updates
    data_disconnect

    # get status of transfer
    ret = nil
    if (type == "get")
      ret = recv_ftp_resp(nsock)
      ret = [ ret, data ]
    else
      ret = recv_ftp_resp(nsock)
    end

    ret
  end

  #
  # This method transmits a FTP command and waits for a response.  If one is
  # received, it is returned to the caller.
  #
  def raw_send_recv(cmd, nsock = self.sock)
    nsock.put(cmd)
    nsock.get_once(-1, ftp_timeout)
  end

  #
  # This method reads an FTP response based on FTP continuation stuff
  #
  def recv_ftp_resp(nsock = self.sock)
    found_end = false
    resp = ""
    left = ""
    if !@ftpbuff.empty?
      left << @ftpbuff
      @ftpbuff = ""
    end
    while true
      data = nsock.get_once(-1, ftp_timeout)
      if not data
        @ftpbuff << resp
        @ftpbuff << left
        return data
      end

      got = left + data
      left = ""

      # handle the end w/o newline case
      enlidx = got.rindex(0x0a.chr)
      if enlidx != (got.length-1)
        if not enlidx
          left << got
          next
        else
          left << got.slice!((enlidx+1)..got.length)
        end
      end

      # split into lines
      rarr = got.split(/\r?\n/)
      rarr.each do |ln|
        if not found_end
          resp << ln
          resp << "\r\n"
          if ln.length > 3 and ln[3,1] == ' '
            found_end = true
          end
        else
          left << ln
          left << "\r\n"
        end
      end
      if found_end
        @ftpbuff << left
        print_status("FTP recv: #{resp.inspect}") if datastore['FTPDEBUG']
        return resp
      end
    end
  end

  #
  # This method transmits a FTP command and does not wait for a response
  #
  def raw_send(cmd, nsock = self.sock)
    print_status("FTP send: #{cmd.inspect}") if datastore['FTPDEBUG']
    nsock.put(cmd)
  end

  ##
  #
  # Wrappers for getters
  #
  ##

  #
  # Returns the user string from the 'FTPUSER' option.
  #
  def user
    datastore['FTPUSER']
  end

  #
  # Returns the user string from the 'FTPPASS' option.
  #
  def pass
    datastore['FTPPASS']
  end

  #
  # Returns the number of seconds to wait for a FTP reply
  #
  def ftp_timeout
    (datastore['FTPTimeout'] || 10).to_i
  end

protected

  #
  # This attribute holds the banner that was read in after a successful call
  # to connect or connect_login.
  #
  attr_accessor :banner, :datasocket

end

end
